In [ ]:
#!pip install plotly
#!pip install folium

Auswertung von Daten der deutschen Bahn¶

Einleitung¶

schienen

Schienennetz vor dem Kollaps - so titeln Marie Blöcher, Nils Naber und Isabel Schneider vom NDR. Die Deutsche Bahn habe zwar ehrgeizige Ziele, allerdings ist Jahrelang zu wenig Geld ins Netz geflossen.
Rund 60 Milliarden Euro müssten laut DB ausgegeben werden, um alle Probleme im Netz zu beheben, die sich über die vergangenen Jahre angesammelt haben. Der Zustand von Strecken und Gleisen wurde über viele Jahre vernachlässigt, sagt Bahnexperte Christian Böttger.

Die Bahn steht aktuell in keinem guten Licht. Zu viele Verspätungen, Zugausfälle und marode Infrastruktur.
Doch wie steht es wirklich um den Zustand der Bahn?

In dieser Analyse wird auf Daten der Deutschen Bahn zugegriffen, um dieser Frage auf den Grund zu gehen.
Die Bahn stellt über den API Marketplace eine Fülle an Daten offen und kostenfrei zur Verfügung.

schienen

Im Rahmen dieses Projekts betrachen wir folgende Daten:

  • Als Grundlage dienst RIS::Stations, darüber lassen sich alle deutschen Bahnhöfe abrufen.
  • FaSta - Station Facility Status gibt Auskunft über den Zustand der Bahnhöfe
  • Railway-Stations Pictures ermöglich den Zugriff auf Bilder jedes einzelnen Bahnhofs.
  • ...

Daten abrufen¶

Über folgende APIs werden die Daten im JSON-Format abgerufen, in pandas dataframes umgewandelt und gespeichert.

url_parking = 'https://apis.deutschebahn.com/db-api-marketplace/apis/parking-information/db-bahnpark/v2/'
url_ris_stations = 'https://apis.deutschebahn.com/db-api-marketplace/apis/ris-stations/v1/'
url_rw_stations = 'https://apis.deutschebahn.com/db-api-marketplace/apis/api.railway-stations.org/photoStationById/'
url_facility_stations = 'https://apis.deutschebahn.com/db-api-marketplace/apis/fasta/v2/stations/'
url_parking_facilities = 'https://apis.deutschebahn.com/db-api-marketplace/apis/parking-information/db-bahnpark/v2/parking-facilities'

Häufig könne nicht alle Daten auf einmal abgerufen werden, daher müssen mehrere Aufrufe gemacht werden. Anschlißend ist ein mapping der (json) DB-Datenstruktur auf einen (flaches) Data Frame nötig.

Da der Abruf der Daten einige Minuten dauert, ist dieser und die eigentliche Auswertung getrennt in zwei verschiedenen files.

Imports¶

Wir nutzen Dateien im Pickel-Format, um ganze Dataframes zwischen dem Scraping-Process und der Datenauswertung auszutauschen.
Für die Visualisierung wird Plotly und Folium genutzt.

In [ ]:
import pandas as pd
import numpy as np
from datetime import datetime, timedelta

import pickle

import plotly.express as px
import plotly.graph_objects as go
import folium

from geopy.geocoders import Nominatim

Daten laden¶

Hier werden die Daten aus den Pickel-Files in Dataframes geladen. Der Vorteil dabei ist, dass die Struktur und die Datenformate dabei beibehalten werden.
Das Data Cleaning wird allerdings hier umgesetzt.

In [ ]:
data_folder = 'data/'
In [ ]:
def loadData(fileName):
    with open(data_folder + fileName, 'rb') as pkl_file:
        return pickle.load(pkl_file)
In [ ]:
df_stations = loadData('stations.pkl')
df_stopplaces = loadData('stopplaces_new.pkl')
df_facilities = loadData('station_facilities.pkl')

Es kann zwar mehrere Bilder pro Bahnhof geben, dies wurde beim data scraping allerdings bereits berücksichtigt, sodass hier immer genau ein Bild pro Bahnhof vorhanden ist.
Die Bilder sind noch als Dictionary gespeichert, sodass dieser hier noch in ein Pandas DataFrame umgewandelt werden.

In [ ]:
df_station_images = loadData('station_images.pkl')

df_images = pd.DataFrame.from_dict({k: v for k, v in df_station_images.items() if v}).T
df_images.columns = ['image']
df_images = df_images.reset_index()

Wir sehen, dass die verschiedenen Daten, die eigentlich zusammengehören sollten, von der Anzahl her nicht komplett zusammenpassen.
Es ist aber durchaus erklärlich, dass es mehr Einrichtungen und Haltestellen als tatsächliche Bahnhöfe bzw. Bahnhofsgebäude gibt. Wie genau die Daten aussehen, wird im Folgenden geprüft.

In [ ]:
print(f'Stations: {df_stations.shape}')
print(f'Station images: {df_images.shape}')
print(f'Facilities: {df_facilities.shape}')
print(f'Stopplaces: {df_stopplaces.shape}')
Stations: (5690, 16)
Station images: (5627, 2)
Facilities: (3550, 6)
Stopplaces: (5727, 9)

Bahnhöfe¶

Das Data Frame der Bahnhöfe enthält alle Haltestellen der Deutschen Bahn in Deutschland.
Jeder Bahnhof hat eine eindeutige id. Zusältzich wird beispielsweise der Name, die Adresse und Geo-Koordinaten mitgeliefert.

In [ ]:
df_stations.head(3)
Out[ ]:
id name metropolis street houseNumber postalCode city state country stationCategory owner organisationalUnit countryCode latitude longitude timeZone
0 1 Aachen Hbf {} Bahnhofstr. 2a 52064 Aachen Nordrhein-Westfalen DE CATEGORY_2 DB S&S RB West DE 50.767800 6.091499 Europe/Berlin
1 1000 Burkhardswalde-Maxen {} Gesundbrunnen 60c 01809 Müglitztal-Burkhardswalde Sachsen DE CATEGORY_7 DB S&S RB Südost DE 50.925146 13.838369 Europe/Berlin
2 1001 Burkhardtsdorf {} Bahnhofstraße NaN 09235 Burkhardtsdorf Sachsen DE CATEGORY_6 DB Regio-Netze Erzgebirgsbahn (EGB) DE NaN NaN Europe/Berlin

Bilder von Bahnhöfen¶

Nutzer können Bilder zu Bahnhöfen hochladen. Das Data Frame enthält Links zu diesen Bildern. Die Spalte index referenziert die Spalte id der Bahnhöfe.

In [ ]:
df_images.head(3)
Out[ ]:
index image
0 1 https://api.railway-stations.org/photos/de/1_1...
1 1000 https://api.railway-stations.org/photos/de/100...
2 1001 https://api.railway-stations.org/photos/de/100...

Hier ein Beispiel vom Bahnhof in Aachen.

Einrichtungen¶

Die Einrichtungen sind beispielsweise Geräte wie Aufzüge auf Bahnhöfen. Das interessante hier ist, dass auch der Zustand mitgegeben wird.

In [ ]:
df_facilities.head(3)
Out[ ]:
id description operatorname state stateExplanation type
0 1 zu Gleis 1 DB Station&Service ACTIVE available ELEVATOR
1 1 zu Gleis 2/3 DB Station&Service ACTIVE available ELEVATOR
2 1 zu Gleis 6/7 DB Station&Service ACTIVE available ELEVATOR

In Wahrheit gibt es hier genau zwei verschiedene Arten von Einrichtungen: Aufzüge und Rolltreppen.

In [ ]:
df_facilities['type'].unique()
Out[ ]:
array(['ELEVATOR', 'ESCALATOR'], dtype=object)

Haltestellen¶

Haltestellen enthalten viele Informationen der Bahnhöfe, zusätzlich aber auch die Transportmittel und welcher Verkehrsbund hier fährt.

In [ ]:
df_stopplaces.head(3)
Out[ ]:
id name availableTransports transportAssociations countryCode state timeZone latitude longitude
0 1 Aachen Hbf [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... [AAV, VRS] DE NW Europe/Berlin 50.767800 6.091499
1 1000 Burkhardswalde-Maxen [REGIONAL_TRAIN] [VVO] DE SN Europe/Berlin 50.925146 13.838369
2 1001 Burkhardtsdorf [REGIONAL_TRAIN, BUS] [VMS] DE SN Europe/Berlin 50.733196 12.932137

Explorative Datenanalyse¶

Datentypen¶

Als erstest prüfen wir, ob die Data Frames korrekte Datentypen haben und korrigieren sie entsprechend.
Die Spalte id soll immer vom Typ int sein, um sie später besser zusammenführen zu können.

In [ ]:
df_stations['id'] = df_stations['id'].astype(int)
df_stations.dtypes
Out[ ]:
id                      int32
name                   object
metropolis             object
street                 object
houseNumber            object
postalCode             object
city                   object
state                  object
country                object
stationCategory        object
owner                  object
organisationalUnit     object
countryCode            object
latitude              float64
longitude             float64
timeZone               object
dtype: object
In [ ]:
df_stopplaces['id'] = df_stopplaces['id'].astype(int)
df_stopplaces.dtypes
Out[ ]:
id                         int32
name                      object
availableTransports       object
transportAssociations     object
countryCode               object
state                     object
timeZone                  object
latitude                 float64
longitude                float64
dtype: object
In [ ]:
df_facilities['id'] = df_facilities['id'].astype(int)
df_facilities.dtypes
Out[ ]:
id                   int32
description         object
operatorname        object
state               object
stateExplanation    object
type                object
dtype: object
In [ ]:
df_images['index'] = df_images['index'].astype(int)
df_images.dtypes
Out[ ]:
index     int32
image    object
dtype: object

Fehlende Werte¶

Als nächsten wird auf fehlende Werte geprüft, um zu schauen, ob hier etwas zu tun ist.

In [ ]:
df_stations.isna().sum()
Out[ ]:
id                      0
name                    0
metropolis              0
street                  8
houseNumber           893
postalCode              7
city                    4
state                   0
country                 0
stationCategory        12
owner                   0
organisationalUnit      0
countryCode             0
latitude              282
longitude             282
timeZone                0
dtype: int64

Das Hausnummernfeld fehlt sehr oft, da wir diese Information aber hier nicht brauchen, ist das kein Problem.

Die Angaben für Latitude und Longitude fehlen auch häufig, hier kann über die Adresse versucht werden, die Werte herauszufinden.
Da das Nachschauen der Werte einige Zeit in Anspruch nimmt, ist dieser Code auskommentiert.

Grundsätzlich wird aber anhand der Spalten postalCode, city, state und country mithilfe des Pakets aus der Vorlesung geopy versucht, die Geo-Koordinaten aufzulösen.

In [ ]:
# geolocator = Nominatim(user_agent="my_app")

# filtered_rows = df_stations[df_stations['latitude'].isnull()]
# result = {}
# # Print the entire row for each entry with NaN latitude
# for index, row in filtered_rows.iterrows():
#     try:
#         address = f'{row["postalCode"]} {row["city"]} {row["state"]} {row["country"]}'
#         result[row['id']] = geolocator.geocode(address)
#     except:
#         pass
In [ ]:
# dict={}
# # extract lat/lon
# for index, entry in result.items():
#     if entry:
#         dict[index] = { 'latitude': entry[1][0], 'longitude': entry[1][1] }

# geocode_result = pd.DataFrame().from_dict(dict).T
# geocode_result['id'] = geocode_result.index
# geocode_result['id'] = geocode_result['id'].astype(int)

Die Ergebnisse landen wieder in einem Pickel-File

In [ ]:
# output = open(data_folder + 'manual_geocode_results.pkl', 'wb')
# pickle.dump(geocode_result, output)
# output.close()
In [ ]:
geocode_result = loadData('manual_geocode_results.pkl')
In [ ]:
df_stations
Out[ ]:
id name metropolis street houseNumber postalCode city state country stationCategory owner organisationalUnit countryCode latitude longitude timeZone
0 1 Aachen Hbf {} Bahnhofstr. 2a 52064 Aachen Nordrhein-Westfalen DE CATEGORY_2 DB S&S RB West DE 50.767800 6.091499 Europe/Berlin
1 1000 Burkhardswalde-Maxen {} Gesundbrunnen 60c 01809 Müglitztal-Burkhardswalde Sachsen DE CATEGORY_7 DB S&S RB Südost DE 50.925146 13.838369 Europe/Berlin
2 1001 Burkhardtsdorf {} Bahnhofstraße NaN 09235 Burkhardtsdorf Sachsen DE CATEGORY_6 DB Regio-Netze Erzgebirgsbahn (EGB) DE NaN NaN Europe/Berlin
3 1002 Bürstadt {} Bahnhofsallee 17 68642 Bürstadt Hessen DE CATEGORY_6 DB S&S RB Mitte DE 49.645769 8.458188 Europe/Berlin
4 1005 Buschow {} Bahnhofstr. 28 14715 Märkisch Luch OT Buschow Brandenburg DE CATEGORY_6 DB S&S RB Ost DE 52.592203 12.628996 Europe/Berlin
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
5685 995 Burgstädt {} Bahnhofstr. 1 09217 Burgstädt Sachsen DE CATEGORY_6 DB S&S RB Südost DE 50.915817 12.812707 Europe/Berlin
5686 996 Burgstall (Murr) {} Bahnhofstr. 1 71576 Burgstetten Baden-Württemberg DE CATEGORY_6 DB S&S RB Südwest DE 48.928647 9.369932 Europe/Berlin
5687 997 Steinfurt-Burgsteinfurt {} Bahnhofsplatz 6 48565 Steinfurt-Burgsteinfurt Nordrhein-Westfalen DE CATEGORY_6 DB S&S RB West DE 52.147384 7.329340 Europe/Berlin
5688 998 Burgthann {} Bahnhofstr. 40 90559 Burgthann Bayern DE CATEGORY_5 DB S&S RB Süd DE 49.342474 11.309307 Europe/Berlin
5689 999 Regensburg-Burgweinting {} Alfons-Goppel-Straße NaN 93055 Regensburg Bayern DE CATEGORY_6 DB S&S RB Süd DE 48.990725 12.146486 Europe/Berlin

5690 rows × 16 columns

Nun liegen die vorhanden Geo-Koordinaten im Data Frame df_stations und die neu ermittelten in geocode_result.
Um nun das Ergebnis aus beiden Tabellen zu bekommen, wird die Funktion combine_first genutzt.

In [ ]:
df_stations = df_stations.set_index('id').combine_first(geocode_result.set_index('id')).reset_index()

Anstelle von 282 fehlenden Werten sind es jetzt nur noch 24!
Die restlichen werden aufgrund der geringen Anzahl ignoriert.

In [ ]:
df_stations.isna().sum()
Out[ ]:
id                      0
city                    4
country                 0
countryCode             0
houseNumber           893
latitude               24
longitude              24
metropolis              0
name                    0
organisationalUnit      0
owner                   0
postalCode              7
state                   0
stationCategory        12
street                  8
timeZone                0
dtype: int64
In [ ]:
df_stopplaces.isna().sum()
Out[ ]:
id                        0
name                      0
availableTransports       0
transportAssociations     0
countryCode               0
state                    11
timeZone                  0
latitude                  0
longitude                 0
dtype: int64

Die Haltestellen scheinen eine bessere Datenqualität zu haben, hier gibt es keine Probleme, die betrachtet werden müssen.

In [ ]:
df_facilities.isna().sum()
Out[ ]:
id                   0
description         51
operatorname         0
state                0
stateExplanation     0
type                 0
dtype: int64

Einige Beschreibungen sind leer, hier muss geprüft werden, ob es sich dabei evtl. immer um den gleichen Typ handelt. Das gute ist, dass werder type noch state jemals leer ist.

Bringen wir die Daten zusammen!¶

Jetzt können alle Daten zusammengebracht werden.

Die Facilities können dabei nicht gejoined werden, da ein Bahnhof in der Regel mehrere davon aufweist (1:n Beziehung)

Dazu werden zunächst alle doppelten Spalten entfernt und dann die Tabellen mithilfe zweier merge zusammengefügt.
Davor wird nochmal stichprobenartig geprüft, ob die IDs auch wirklich zusammenpassen.

In [ ]:
df_stopplaces[df_stopplaces['name'] == 'Ahrensfelde']
Out[ ]:
id name availableTransports transportAssociations countryCode state timeZone latitude longitude
1539 28 Ahrensfelde [REGIONAL_TRAIN] [VBB] DE BE Europe/Berlin 52.571375 13.565154
In [ ]:
df_stations[df_stations['name'] == 'Ahrensfelde']
Out[ ]:
id city country countryCode houseNumber latitude longitude metropolis name organisationalUnit owner postalCode state stationCategory street timeZone
23 28 Berlin DE DE NaN 52.571375 13.565154 {} Ahrensfelde RB Ost DB S&S 12689 Berlin CATEGORY_4 Märkische Allee Europe/Berlin
In [ ]:
df_stopplaces.drop(columns=['name', 'state', 'countryCode', 'latitude', 'longitude','timeZone'], inplace=True)
df = pd.merge(df_stations, df_stopplaces, on='id', how='left')
df = pd.merge(left=df, right=df_images, left_on=['id'], right_on=['index'], how='left')
df.drop(columns=['timeZone','index','country'], inplace=True)
In [ ]:
df.isna().sum()
Out[ ]:
id                         0
city                       4
countryCode                0
houseNumber              903
latitude                  24
longitude                 24
metropolis                 0
name                       0
organisationalUnit         0
owner                      0
postalCode                 7
state                      0
stationCategory           12
street                     8
availableTransports       18
transportAssociations     18
image                     64
dtype: int64

Es fehlen nun noch einzelne Werte, aber mit dieser Datengrundlage kann gut gearbeitet werden.

Einfache Datenvisualisierung¶

Schauen wir uns an, was wir an Daten haben und welche Fragen sich daraus ergeben:

  • Wir können überprüfen, ob wir tatsächlich nur deutsche Stationen haben.
  • Wir können überprüfen, wie die organisationalUnit mit dem owner korrelieren, vielleicht auch mit transportAssociations.
  • Wir können die verfügbaren transportAssociations überprüfen.
  • Natürlich können wir uns die Geo-Koordinaten anschauen.
  • Es ist noch fraglich, wofür die Werte von stationCategory stehen.
In [ ]:
df.head(1)
Out[ ]:
id city countryCode houseNumber latitude longitude metropolis name organisationalUnit owner postalCode state stationCategory street availableTransports transportAssociations image
0 1 Aachen DE 2a 50.7678 6.091499 {} Aachen Hbf RB West DB S&S 52064 Nordrhein-Westfalen CATEGORY_2 Bahnhofstr. [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... [AAV, VRS] https://api.railway-stations.org/photos/de/1_1...

Bahnhöfe der Schweiz¶

Es gibt im Datenbestand auch einige Bahnhöfe, die in der Schweiz liegen.

Beispielsweise Schaffhausen ist ein Gemeinschaftsbahnhof zwischen der Schweizerischen Bundesbahnen und dem deutschen Bundeseisenbahnvermögen. Quelle: Wikipedia

Auffällig ist, dass bei diesen Bahnhöfen keine Bilder und auch keine stationCategory / transportAssociations vorhanden sind, das scheint nur im Datenbestand der deutschen Bahnhöfe zu exisiteren.

In [ ]:
df[df['countryCode']!='DE']
Out[ ]:
id city countryCode houseNumber latitude longitude metropolis name organisationalUnit owner postalCode state stationCategory street availableTransports transportAssociations image
347 424 Basel CH 200 47.567288 7.607805 {} Basel Bad Bf RB Südwest DB S&S 4016 Schweiz CH NaN Schwarzwaldallee [INTERCITY_TRAIN, REGIONAL_TRAIN, HIGH_SPEED_T... [RVL] NaN
2093 2698 Schaffhausen CH 1 47.717003 8.664127 {} Herblingen RB Südwest DB S&S 8207 Schweiz CH NaN Bruderhalde [CITY_TRAIN] [] NaN
3368 4399 Neuhausen CH 18 47.682615 8.612186 {} Neuhausen Bad Bf RB Südwest DB S&S 8212 Schweiz CH NaN Badischen Bahnhofstr. [REGIONAL_TRAIN] [] NaN
3387 4424 Neunkirch CH 3 47.689151 8.495384 {} Neunkirch RB Südwest DB S&S 8225 Schweiz CH NaN Bahnhofstr. [REGIONAL_TRAIN] [] NaN
4053 5274 Riehen CH 25 47.583157 7.652014 {} Riehen RB Südwest DB S&S 4125 Schweiz CH NaN Bahnhofstr. [REGIONAL_TRAIN] [RVL] NaN
4233 5530 Schaffhausen CH 29 NaN NaN {} Schaffhausen RB Südwest DB S&S 8200 Schweiz CH NaN Bahnhofstr. NaN NaN NaN
4715 6192 Thayngen CH 31 47.745502 8.704300 {} Thayngen RB Südwest DB S&S 8240 Schweiz CH NaN Bahnhofstr. [CITY_TRAIN] [] NaN
4743 6235 Trasadingen CH 1 47.665238 8.436804 {} Trasadingen RB Südwest DB S&S 8219 Schweiz CH NaN Bahnhofstr. [REGIONAL_TRAIN] [] NaN
5128 6762 Wilchingen CH 18 47.679448 8.463860 {} Wilchingen-Hallau RB Südwest DB S&S 8217 Schweiz CH NaN Bahnhofstrasse [REGIONAL_TRAIN] [] NaN

Betreiber und Bahnhofdichte je Bundesland¶

In [ ]:
def plot_counts(column):
    counts = df[column].value_counts().reset_index()
    counts.columns = [column, 'count']

    fig = px.bar(counts, x=column, y='count', barmode='group', text='count')
    fig.show()

Die meisten Bahnhöfe gehören der DB Station&Service AG. Laut ihrer Webseite, unterhalten sie rund 5.400 Bahnhöfe.

Das können wir bestätigen! The DB S&S hat laut den Daten 5.413 Bahnhöfe. Der Rest, 277, werden von der DB Regio-Netze unterhalten.

In [ ]:
plot_counts('owner')

Betrachtet man die Organisationsbereiche, gibt es viele Stationen in der mitte/süden/westen von Deutschland. Der Norden und Osten liegen hingegen auf den letzten Plätzen.

Es gibt auch einige kleinere Organisationseinheiten für spezielle Regionen.

In [ ]:
plot_counts('organisationalUnit')

Tatsächlich sehen wir, dass die führenden Bundesländer Bayern, Baden-Württemberg und Nordrhein-Westfalen (NRW) sind.
Es besteht eine deutliche Lücke zwischen ihnen und dem viertplatzierten Bundesland Hessen. Natürlich müssen wir auch die Größe der Bundesländer berücksichtigen.

Wir können prüfen, welches Bundesland laut seiner Größe die meisten Bahnhöfe hat. Dazu rufen wir die Größe der Bundesländer ab und setzen sie ins Verhältnis mit der Anzahl der Stationen.

In [ ]:
df_states = pd.read_csv(data_folder + 'states_size.csv', sep=';')
df_states['size'] = df_states['size'].astype(float)
df_states
Out[ ]:
state size
0 Baden-Württemberg 35747.82
1 Bayern 70541.57
2 Berlin 891.12
3 Brandenburg 29654.35
4 Bremen 419.62
5 Hamburg 755.09
6 Hessen 21115.64
7 Mecklenburg-Vorpommern 23295.45
8 Niedersachsen 47709.82
9 Nordrhein-Westfalen 34112.44
10 Rheinland-Pfalz 19858.00
11 Saarland 2571.11
12 Sachsen 18449.93
13 Sachsen-Anhalt 20459.12
14 Schleswig-Holstein 15804.30
15 Thüringen 16202.39
16 Deutschland 357587.77
In [ ]:
df_grp_states = pd.DataFrame(df_stations.groupby(by='state').count()['id'].sort_values(ascending=False))
df_grp_states.rename(columns={'id': 'count'}, inplace=True)
df_grp_states
Out[ ]:
count
state
Bayern 1025
Baden-Württemberg 720
Nordrhein-Westfalen 711
Hessen 479
Sachsen 478
Rheinland-Pfalz 419
Niedersachsen 357
Brandenburg 310
Sachsen-Anhalt 289
Thüringen 289
Mecklenburg-Vorpommern 180
Schleswig-Holstein 137
Berlin 133
Saarland 77
Hamburg 58
Bremen 16
Schweiz CH 12
In [ ]:
df_germany = pd.DataFrame(index=['count'], data={
    'Deutschland':df_grp_states.sum().values[0]
}).T

df_grp_states = pd.concat([df_grp_states, df_germany])
df_grp_states['state'] = df_grp_states.index
df_grp_states = pd.merge(df_states, df_grp_states, how='left', on='state')
In [ ]:
df_grp_states
Out[ ]:
state size count
0 Baden-Württemberg 35747.82 720
1 Bayern 70541.57 1025
2 Berlin 891.12 133
3 Brandenburg 29654.35 310
4 Bremen 419.62 16
5 Hamburg 755.09 58
6 Hessen 21115.64 479
7 Mecklenburg-Vorpommern 23295.45 180
8 Niedersachsen 47709.82 357
9 Nordrhein-Westfalen 34112.44 711
10 Rheinland-Pfalz 19858.00 419
11 Saarland 2571.11 77
12 Sachsen 18449.93 478
13 Sachsen-Anhalt 20459.12 289
14 Schleswig-Holstein 15804.30 137
15 Thüringen 16202.39 289
16 Deutschland 357587.77 5690
  • Gemäß seiner Größe hat Berlin die meisten Haltestellen, wobei jede Station durchschnittlich etwa 6,7 km² abdeckt. Für eine Hauptstadt ergibt dies Sinn.
  • Im Allgemeinen haben alle großen Städte (Hamburg, Bremen) dieses Verhältnis.
  • Zuvor waren Bayern und Baden-Württemberg die Regionen mit den meisten Stationen, nun befinden sie sich auf den Plätzen 11 und 8.
  • Das Saarland, das die wenigsten Bahnhöfe hat, weist das beste Verhältnis der Fläche zu Stationen unter den Bundesländern auf.
  • In Niedersachsen muss eine Station im Durchschnitt etwa 133,6 km² abdecken, das ist viel, wenn man so weit fahren muss, um zur nächsten Station zu gelangen.
  • Der Durchschnitt für ganz Deutschland liegt bei 62 km².
In [ ]:
df_grp_states['ratio'] = df_grp_states['size']/df_grp_states['count']
df_grp_states.sort_values('ratio', ascending=True).reset_index().drop(columns=['index'])
Out[ ]:
state size count ratio
0 Berlin 891.12 133 6.700150
1 Hamburg 755.09 58 13.018793
2 Bremen 419.62 16 26.226250
3 Saarland 2571.11 77 33.391039
4 Sachsen 18449.93 478 38.598180
5 Hessen 21115.64 479 44.082756
6 Rheinland-Pfalz 19858.00 419 47.393795
7 Nordrhein-Westfalen 34112.44 711 47.978115
8 Baden-Württemberg 35747.82 720 49.649750
9 Thüringen 16202.39 289 56.063633
10 Deutschland 357587.77 5690 62.844951
11 Bayern 70541.57 1025 68.821044
12 Sachsen-Anhalt 20459.12 289 70.792803
13 Brandenburg 29654.35 310 95.659194
14 Schleswig-Holstein 15804.30 137 115.359854
15 Mecklenburg-Vorpommern 23295.45 180 129.419167
16 Niedersachsen 47709.82 357 133.640952

Um das ganze auf einer Karte anzuzeigen, fehlen uns noch die Geo-Koordinaten der Bundesländer. Diese werden wie zuvor abgerufen.

In [ ]:
geolocator = Nominatim(user_agent="my_app")

result={}
for entry in df_grp_states['state']:
        result[entry] = geolocator.geocode(entry)
In [ ]:
res_dict={}
# extract lat/lon
for index, entry in result.items():
    if entry:
        res_dict[index] = { 'latitude': entry[1][0], 'longitude': entry[1][1] }

geocode_result = pd.DataFrame().from_dict(res_dict).T
geocode_result['state'] = geocode_result.index
geocode_result
Out[ ]:
latitude longitude state
Baden-Württemberg 48.537750 9.041169 Baden-Württemberg
Bayern 48.946756 11.403872 Bayern
Berlin 52.517037 13.388860 Berlin
Brandenburg 52.845549 13.246130 Brandenburg
Bremen 53.075820 8.807165 Bremen
Hamburg 53.550341 10.000654 Hamburg
Hessen 50.608065 9.028465 Hessen
Mecklenburg-Vorpommern 53.773506 12.575547 Mecklenburg-Vorpommern
Niedersachsen 52.839853 9.075962 Niedersachsen
Nordrhein-Westfalen 51.478921 7.554375 Nordrhein-Westfalen
Rheinland-Pfalz 49.953160 7.310646 Rheinland-Pfalz
Saarland 49.384187 6.953737 Saarland
Sachsen 50.929580 13.458505 Sachsen
Sachsen-Anhalt 52.008907 11.700334 Sachsen-Anhalt
Schleswig-Holstein 54.185400 9.822009 Schleswig-Holstein
Thüringen 50.901472 11.037784 Thüringen
Deutschland 51.163818 10.447831 Deutschland
In [ ]:
df_grp_states_geo = pd.merge(df_grp_states, geocode_result, how='left', on='state')
df_grp_states_geo.drop(16, inplace=True) # drop germany

In diesem Fall nutzen wir plotly, um die Karte anzuzeigen.

Die Größe der Punkte zeigt die Anzahl der Haltestationen an.
Die Farbe gibt das Verhältnis zur Fläche an. Grün steht für deine hohe Dichte an Stationen, rot für eine niedrige.

Hier sehen wir nochmal, dass die Stadtstaaten eine hohe Dichte an Haltestationen aufweisen, allerdings absolut gesehen wenige Stationen haben (kleiner Kreis).
Die großen Bundesländer sind eher im mittleren Farbschema, wobei die im Norden rot gefärbt und damit Schlusslicht sind.

In [ ]:
fig = px.scatter_geo(df_grp_states_geo,
                     lat='latitude', 
                     lon='longitude',
                     hover_name='state',     # Data to display when hovering over each data point
                     size='count',     # Size of the markers
                     color='ratio',    # Color of the markers
                     color_continuous_scale=['green','orange','red'],
                     projection='mercator',
                     scope='europe',
                     width=650,
                     height=800)  # Map projection

fig.update_geos(center=dict(lon=10, lat=51), projection_scale=10)
fig.show()

Verkehrsmittel und Verkehrsverbünde¶

Da die Verkehrsmittel und Verkehrsverbünde in geschachtelten Listen vorliegen, müssen diese zunächst geebnet werden, um die absolute Anzahl herauszufinden.

In [ ]:
transports = []
for entry in df['transportAssociations']:
    try:
        for e in entry:
            transports.append(e)
    except:
        pass

transportAssociations = pd.Series(transports).value_counts()
In [ ]:
transports = []
for entry in df['availableTransports']:
    try:
        for e in entry:
            transports.append(e)
    except:
        pass

availableTransports = pd.Series(transports).value_counts()

Spitzenreiter ist auch hier wieder Berlin, gefolgt von dem Rhein-Main-Verkehrsverbund (RMV).

Der RMV operiert in Hessen und ist der Nachfolger des Frankfurter Verkehrsverbundes (FVV) (Quelle: Wikipedia) Hier sieht man das Verkehrsgebiet in Hessen.

Die "NASA" ist der Nahverkehrsservice der Sachsen-Anhalt GmbH und hat lustigerweise auch die Webseite https://www.nasa.de/.

Man sieht auf dem Plan, dass hauptsächlich die Städt Leipzig, Halle, Dessau und Magdeburg verbunden werden und der "NASA" aus anderen Verkehrsverbünden bestehen, die ebenfalls hier auftauchen, wie der MDV und Übergänge zu anderen Verkehrsverbünden hat wie VMT, VRB und VBB.

Die in unserer Region bekannteren Verkehrsverbünde VVS und NALDO rangieren auf den mittleren Plätzen.

In [ ]:
def plotBar(data, title, xlabel, ylabel, showlegend):
    fig = px.bar(data, title=title)
    fig.update_xaxes(title_text=xlabel)
    fig.update_yaxes(title_text=ylabel)
    fig.update_traces(showlegend=showlegend)
    return fig
In [ ]:
fig = plotBar(transportAssociations, 'Available Transport Associations', 'Transport Associations', 'Count', False)
fig.show()

Mit großem Abstand gibt es Haltestationen, an denen die REGIONAL_TRAIN hält. Leider ist nicht dokumentiert, was darunter zu verstehen ist.

Nach manueller Untersuchung der Daten werden damit sowohl Regionalbahnen (RB) wie auch S-Bahnen gemeint sein. Allerdings werden die S-Bahn Stationen in der Stadt mit CITY_TRAIN markiert. Das ist also nicht ganz eindeutig.

Neben Zügen werden hier auch Busse (BUS) erfasst sowie CITY_TRAIN, was die Stadtbahnen/Tram/U-Bahn sowie S-Bahnen in der Stadt sind.

Im Vergleich dazu kommen die Schnellzüge INTERCITY_TRAIN (IC), HIGH_SPEED_TRAIN (ICE) und INTER_REGIONAL_TRAIN (IRE) fast schon selten vor. Das macht aber natürlich Sinn, weil diese nur an ausgewählten Bahnhöfen halten.

Hier gibt es eine Übersicht der Bahn über die Nah- und Fernverkehrszüge.

In [ ]:
fig = plotBar(availableTransports, 'Available Transports', 'Transport Type', 'Count', False)
fig.show()

Rolltreppen und Aufzüge¶

In [ ]:
df_facilities.head(1)
Out[ ]:
id description operatorname state stateExplanation type
0 1 zu Gleis 1 DB Station&Service ACTIVE available ELEVATOR

Zunächst bringen wir die Daten in eine Form, die besser zu visualisieren ist.

Uns interessiert, welche Art (type) von facilities in welchem Zustand (state) ist.
Dazu wird anhand dieser beiden Werte gruppiert und die summierten Werte wieder in eine flache Struktur geformt.

Man könnte auch noch auf den operatorname eingehen, allerdings werden die allermeisten Einrichtungen wieder von der DB Station&Service betrieben.

In [ ]:
df_facilities_grouped = df_facilities.groupby(['type', 'state']).count()
df_facilities_grouped = df_facilities_grouped.unstack()['id']
df_facilities_grouped
Out[ ]:
state ACTIVE INACTIVE UNKNOWN
type
ELEVATOR 2392 126 51
ESCALATOR 820 140 21
In [ ]:
df_facilities_grouped['Ratio'] = (df_facilities_grouped['INACTIVE']) / df_facilities_grouped['ACTIVE']
df_facilities_grouped
Out[ ]:
state ACTIVE INACTIVE UNKNOWN Ratio
type
ELEVATOR 2392 126 51 0.052676
ESCALATOR 820 140 21 0.170732

Es gibt ein paar Daten, bei denen der Zustand unbekannt ist.

Darüber hinaus sind absolut und relativ gesehen aktuell mehr Rolltreppen kaputt als Aufzüge.
Relativ sind es mit über 15% (nach update eine Woche später: 17%) aktuell nicht funktionierende Rolltreppen wirklich viele.

In [ ]:
fig = go.Figure()

for state in df_facilities_grouped.columns[:3]:
    fig.add_trace(go.Bar(
        x=df_facilities_grouped.index,
        y=df_facilities_grouped[state],
        name=state,
    ))

fig.update_layout(title='Zustand der Einrichtungen an Bahnhöfen',
                  xaxis_title='Typ',
                  yaxis_title='Anzahl',
                  barmode='group')

fig.show()

Dashboard für Haltestationen¶

Wir können uns eine Karte anzeigen lassen, die alle Haltestationen mit zusätzliche Informationen anzeigt.

Ganz Deutschland anzuzeigen führt allerding zu Performanceproblemen, daher werden zunächst alle Marker ausgeblendet und können über den Filter je Bundesland hinzugeschaltet werden.

In [ ]:
df.dropna(subset = ['latitude'], inplace=True)

Dazu wird zuerst pro Bundesland eine FeatureGroup erstellt, die initial ausgeblendet ist.

In [ ]:
state_dict = {}
for i in df.index:
    state_dict.setdefault(df['state'][i], folium.FeatureGroup(name=df['state'][i], show=False, autoZIndex=False))
state_dict
Out[ ]:
{'Nordrhein-Westfalen': <folium.map.FeatureGroup at 0x1b6a14e2610>,
 'Baden-Württemberg': <folium.map.FeatureGroup at 0x1b69fda00a0>,
 'Bayern': <folium.map.FeatureGroup at 0x1b6e12cb130>,
 'Niedersachsen': <folium.map.FeatureGroup at 0x1b6a0460760>,
 'Sachsen': <folium.map.FeatureGroup at 0x1b6a140c070>,
 'Schleswig-Holstein': <folium.map.FeatureGroup at 0x1b6a140cca0>,
 'Berlin': <folium.map.FeatureGroup at 0x1b6a140ca60>,
 'Brandenburg': <folium.map.FeatureGroup at 0x1b6a140cbe0>,
 'Rheinland-Pfalz': <folium.map.FeatureGroup at 0x1b6a140ceb0>,
 'Hessen': <folium.map.FeatureGroup at 0x1b6a140cd30>,
 'Hamburg': <folium.map.FeatureGroup at 0x1b6a140caf0>,
 'Mecklenburg-Vorpommern': <folium.map.FeatureGroup at 0x1b6a140c820>,
 'Thüringen': <folium.map.FeatureGroup at 0x1b6a140c520>,
 'Sachsen-Anhalt': <folium.map.FeatureGroup at 0x1b6a140c970>,
 'Saarland': <folium.map.FeatureGroup at 0x1b6a140c4c0>,
 'Schweiz CH': <folium.map.FeatureGroup at 0x1b6a140c430>,
 'Bremen': <folium.map.FeatureGroup at 0x1b6a140c400>}

Per HTML kann ein Popup definiert werden, das erscheint, wenn man auf den Pin klickt. Hierbei wird das Bild und die zugehörigen Verkehrsverbünde und Zugtypen angezeigt.

Auf dieser Karte kann man sehr schön erkennen, wo die Bahnlienien verlaufen und dass es gewisse Regionen gibt, die nicht an die Bahn angeschlossen sind.
Beim Klicken durch die Bilder fällt auch auf, dass die Bahnhöfe häufig ältere Gebäude sind, von den allerdings viele Renoviert wurden.

Die roten Marker identifizieren Bahnhöfe in denen ICEs halten, in den gelben "nur noch" die IREs.

In [ ]:
def GetIcon(availableTransports):
    try:
        if ('HIGH_SPEED_TRAIN' in availableTransports):
            return folium.Icon(color='red', icon='map-marker')
        elif ('INTERCITY_TRAIN' in availableTransports):
            return folium.Icon(color='orange', icon='map-marker')
    except:
        return folium.Icon(color='blue', icon='map-marker')
In [ ]:
map_df = df

m = folium.Map(location=[50.111, 8.682],zoom_start=6) # limit with width=1500,height=1500 produces just white space around the map.

for i in map_df.index:
    html=f"""
    <img src="{map_df['image'][i]}" width="500px">
    <br/>
    <b><p>{map_df['id'][i]}: {map_df['name'][i]}</b></p>
    <p>Transports: {map_df['availableTransports'][i]}</p>
    <p>Associations: {map_df['transportAssociations'][i]}</p>
    """

    parsedHtml = folium.Html(html, script=True)
    popup = folium.Popup(parsedHtml, max_width=2650)

    # this is probably done too often, but folium is smart enough
    feature_group = state_dict[map_df['state'][i]]
    m.add_child(feature_group)

    folium.Marker(
        location=[ map_df['latitude'][i], map_df['longitude'][i] ], 
        icon=GetIcon(map_df['availableTransports'][i]),
        radius=8,
        tooltip=map_df['name'][i],
        popup=popup
    ).add_to(feature_group)
    
folium.LayerControl(collapsed=False).add_to(m)
m
Out[ ]:
Make this Notebook Trusted to load map: File -> Trust Notebook
In [ ]:
 

Bahnhofsnahe Dienstleistungen¶

Mögliche Werte von Type aus der Dokumentation:

  • INFORMATION_COUNTER [Informationsstand für Belange im Bahnhof (kein Fahrkartenverkauf)]
  • TRAVEL_CENTER [Reisezentrum]
  • VIDEO_TRAVEL_CENTER [Video Reisezentrum]
  • TRIPLE_S_CENTER [3S Zentrale für Service, Sicherheit & Sauberkeit]
  • TRAVEL_LOUNGE [Lounge (DB Lounge z.B.)]
  • LOST_PROPERTY_OFFICE [Fundstelle]
  • RAILWAY_MISSION [Bahnhofsmission]
  • HANDICAPPED_TRAVELLER_SERVICE [Service für mobilitätseingeschränkte Reisende]
  • LOCKER [Schließfächer]
  • WIFI [WLan]
  • CAR_PARKING [Autoparkplatz, ggf. kostenpflichtig]
  • BICYCLE_PARKING [Fahrradparkplätze, ggf. kostenpflichtig]
  • PUBLIC_RESTROOM [Öffentliches WC, ggf. kostenpflichtig]
  • TRAVEL_NECESSITIES [Geschäft für den Reisendenbedarf]
  • CAR_RENTAL [Car-Sharer oder Mietwagen]
  • BICYCLE_RENTAL [Mieträder]
  • TAXI_RANK [Taxi Stand]
  • MOBILE_TRAVEL_SERVICE [Mobiler Service]
  • RAD_PLUS (Rad+ Gebiet)
In [ ]:
df_local_services = loadData('local_services.pkl')
In [ ]:
df_local_services.head()
Out[ ]:
id name description openingHours latitude longitude type
0 1 None None Mo-Su 06:15-22:30;PH 06:15-22:30 MOBILE_TRAVEL_SERVICE
1 1 Duisburg Hbf None Mo-Su 00:00-24:00;PH 00:00-24:00 TRIPLE_S_CENTER
2 1 None None None RAILWAY_MISSION
3 1 None Ja, um Voranmeldung unter 030 65 21 28 88 (Ort... None HANDICAPPED_TRAVELLER_SERVICE
4 1 None None None LOCKER

Man erkennt, dass die TRIPE_S_CENTER (Service, Sicerheit & Sauberkeit), TRAVEL_CENTER, TRAVEL_LOUNGE und VIDEO_TRAVEL_CENTER alle einen Namen, Öffnungszeiten und Geo-Koordinaten haben.

Die Services RAD_PLUS und TRAVEL_LOUNGE haben einen Namen, MOBILE_TRAVEL_SERVICE, INFORMATION_COUNTER und LOST_PROPERTY_OFFICE dafür Öffnungszeiten.

In [ ]:
df_local_services.groupby(by='type').count().sort_values(by='id', ascending=False)
Out[ ]:
id name description openingHours latitude longitude
type
TRIPLE_S_CENTER 4103 4103 0 4103 4103 4103
CAR_PARKING 3136 0 0 0 3136 3136
BICYCLE_PARKING 3081 0 0 0 3081 3081
TAXI_RANK 1100 0 0 0 1100 1100
PUBLIC_RESTROOM 562 0 0 0 562 562
TRAVEL_NECESSITIES 508 0 0 0 508 508
HANDICAPPED_TRAVELLER_SERVICE 318 0 233 0 318 318
TRAVEL_CENTER 260 260 260 260 260 260
RAD_PLUS 260 260 0 0 260 260
LOCKER 168 0 0 0 168 168
MOBILE_TRAVEL_SERVICE 127 0 0 127 127 127
WIFI 119 0 0 0 119 119
RAILWAY_MISSION 89 0 0 0 89 89
VIDEO_TRAVEL_CENTER 87 87 87 87 87 87
INFORMATION_COUNTER 76 0 0 76 76 76
LOST_PROPERTY_OFFICE 70 0 70 70 70 70
CAR_RENTAL 67 0 0 0 67 67
TRAVEL_LOUNGE 14 14 0 14 14 14
In [ ]:
def getDataByType(type):
    filtered_ids = df_local_services[df_local_services['type'] == type]['id']

    count = filtered_ids.count()
    unique_count = filtered_ids.nunique()

    print(type,"Count:", count)
    print(type,"unique Count:", unique_count)

    return df_local_services[df_local_services['type'] == type]
In [ ]:
df_triples_center = getDataByType('TRIPLE_S_CENTER')
TRIPLE_S_CENTER Count: 4103
TRIPLE_S_CENTER unique Count: 4103

Wenn man sich die Daten anschaut, tauchen dort viele IDs doppelt auf und es scheint, als wären die Daten schwierig zu interpretieren.
Gruppiert man die Daten allerdings anhand des Typs, sind die IDs eindeutig und können somit gut analysiert werden.

In [ ]:
df_triples_center.head(3)
Out[ ]:
id name description openingHours latitude longitude type
1 1 Duisburg Hbf None Mo-Su 00:00-24:00;PH 00:00-24:00 TRIPLE_S_CENTER
13 1000 Dresden None Mo-Su 00:00-24:00;PH 00:00-24:00 TRIPLE_S_CENTER
16 1002 Frankfurt (Main) Hbf None Mo-Su 00:00-24:00;PH 00:00-24:00 TRIPLE_S_CENTER

Die Öffnungszeiten sind in einem bestimmten Format angegeben, daher müssen diese erst geparst werden.
Zuvor wird aber geprüft, ob es überhaupt abweichungen gibt.

In [ ]:
df_triples_center['openingHours'].unique()
Out[ ]:
array(['Mo-Su 00:00-24:00;PH 00:00-24:00'], dtype=object)

Scheinbar haben alle 3S-Center durchgehend geöffnet.

In [ ]:
df_travel_center = getDataByType('TRAVEL_CENTER')
TRAVEL_CENTER Count: 260
TRAVEL_CENTER unique Count: 257
In [ ]:
df_travel_center.head(1)
Out[ ]:
id name description openingHours latitude longitude type
11 1 DB Reisezentrum Aachen Hbf Mo-Fr 06:00-21:00;Sa 07:00-20:00;Su 08:00-20:00 50.768944 6.0902 TRAVEL_CENTER

Bei den Reisezentren sehen die Öffnungszeiten schon interessanter aus.

In [ ]:
df_travel_center['openingHours'].unique()
Out[ ]:
array(['Mo-Fr 06:00-21:00;Sa 07:00-20:00;Su 08:00-20:00',
       'Mo-We 08:00-12:30,13:00-17:00;Th-Fr 08:00-12:30,13:00-18:00;Sa 08:00-13:30',
       'Mo-Fr 08:00-17:00;Sa 08:00-13:00;Su 10:00-15:00',
       'Mo 06:00-11:00,12:00-16:00;Tu-We,Fr 08:00-13:00,14:00-16:00;Th 09:00-13:00,14:00-19:00;Sa 08:00-12:00',
       'Mo-Fr 06:30-12:00,13:00-18:30;Sa 08:00-13:00',
       'Mo-Su 07:00-21:00',
       'Mo-Fr 07:30-18:30;Sa-Su 09:00-13:00,13:30-17:00',
       'Mo-Fr 09:00-12:00,13:00-17:25',
       'Mo-Fr 08:00-12:00,13:00-18:00;Sa 08:30-13:30',
       'Mo-Fr 08:00-18:00', 'Mo-Fr 07:00-19:00;Sa 09:00-14:00',
       'Mo-Fr 07:00-11:30,12:00-14:30;Sa 08:30-13:30',
       'Mo-Fr 06:30-09:00,09:30-17:30',
       'Mo 06:30-18:30;Tu-Fr 07:30-18:30;Sa 06:30-11:30,12:00-14:30;Su 10:30-14:30,15:00-18:30',
       'Mo-Fr 06:30-12:00,13:00-18:30', 'Mo-Fr 09:00-12:30,13:30-17:00',
       'Mo 06:00-10:30,11:30-16:00;Tu 09:30-13:30,14:30-19:00;We-Fr 08:30-12:30,13:30-16:00;Sa 08:00-12:00',
       'Mo-Fr 09:00-12:30,14:00-17:45',
       'Mo 06:00-11:30,12:20-16:30;Tu-Fr 07:00-11:30,12:15-16:35',
       'Mo-Fr 07:30-19:00;Sa-Su 09:00-18:00',
       'Mo,Fr 06:15-16:35 open "Am 01.08.23 ist das Reisezentrum ;krankheitsbedingt geschlossen. ;Wir bitten um Ihr Verständnis. ";Tu-Th 06:15-12:00,12:45-16:35 open "Am 01.08.23 ist das Reisezentrum ;krankheitsbedingt geschlossen. ;Wir bitten um Ihr Verständnis. ";Sa 07:10-12:00 open "Am 01.08.23 ist das Reisezentrum ;krankheitsbedingt geschlossen. ;Wir bitten um Ihr Verständnis. "',
       'Mo-Fr 06:45-20:00;Sa-Su 07:00-20:00',
       'Mo-Fr 08:00-18:00;Sa 09:00-16:00', 'Mo-Fr 06:30-11:45',
       'Mo-Fr 07:00-18:00;Sa 08:00-12:00,12:30-14:30',
       'Mo-Fr 07:00-21:00;Sa-Su 09:00-21:00',
       'Mo 06:00-15:00,15:45-20:00;Tu-Fr 07:00-12:45,13:45-17:00;Sa-Su 09:30-15:00',
       'Mo-We,Fr 08:30-13:00,13:45-18:00', 'Mo-Fr 06:30-17:30',
       'Mo-Fr 05:45-18:45;Sa 08:00-13:00',
       'Mo-Fr 07:30-12:30,13:30-17:30;Sa 07:30-12:30',
       'Mo-Fr 08:00-13:00,14:00-17:45',
       'Mo-Fr 07:00-17:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten.";Sa 08:00-13:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten."',
       'Mo-Fr 08:30-18:30;Sa-Su 09:30-16:30',
       'Mo 07:00-12:15,13:00-16:30;Tu-We,Fr 09:00-12:15,13:00-16:30;Th 09:00-12:15,13:00-18:00',
       'Mo-Fr 07:30-19:00;Sa 08:15-15:00;Su 09:15-15:00',
       'Mo-Fr 07:30-18:30;Sa 09:00-14:00',
       'Mo 06:00-17:30;Tu-We,Fr 09:00-17:30;Th 09:00-20:00;Sa 08:00-16:30;Su 09:00-12:30,13:00-16:30',
       'Mo-Fr 06:00-20:00;Sa 07:30-12:15,12:45-15:30',
       'Mo-Fr 07:30-21:00;Sa-Su 08:30-18:30',
       'Mo 06:30-17:50 open "Aufgrund von kurzfristiger Krankheit ist;das Rz Forchheim am Dienstag, 01.08.2023;bis 15:30 Uhr geöffnet!";Tu,Fr 07:00-17:50 open "Aufgrund von kurzfristiger Krankheit ist;das Rz Forchheim am Dienstag, 01.08.2023;bis 15:30 Uhr geöffnet!";We-Th 07:00-11:00,11:45-16:15 open "Aufgrund von kurzfristiger Krankheit ist;das Rz Forchheim am Dienstag, 01.08.2023;bis 15:30 Uhr geöffnet!"',
       'Mo-Fr 07:00-19:00;Sa 07:30-11:30,12:00-16:00;Su 09:30-15:00',
       'Mo-Fr 06:00-20:30;Sa 07:00-14:30',
       'Mo-Fr 07:30-19:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten.";Sa 08:30-15:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten.";Su 11:00-17:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten."',
       'Mo-Fr 07:00-11:30,13:30-18:00;Sa 09:00-14:00',
       'Mo-Fr 07:00-20:00;Sa 08:30-19:00;Su 08:30-20:00',
       'Mo-Fr 08:30-13:00,14:00-18:00;Sa 09:00-13:45',
       'Mo 07:15-12:00,13:00-17:45;Tu-Fr 08:00-12:00,13:00-17:45;Sa 08:00-13:30',
       'Mo-Fr 07:00-18:30 open "Nutzen Sie auch das Video-Reisezentrum im Vorraum.";Sa 08:00-13:30 open "Nutzen Sie auch das Video-Reisezentrum im Vorraum."',
       'Mo-Fr 08:00-12:00,13:00-18:00;Sa 08:00-13:30',
       'Mo-Fr 07:00-18:00;Sa 09:00-14:00',
       'Mo-Fr 08:00-18:30;Sa 08:15-16:30;Su 12:00-17:00',
       'Mo-Fr 07:00-19:00;Sa 09:00-14:30', 'Mo-Fr 07:15-17:15',
       'Mo-Fr 08:30-17:30;Sa 08:30-13:30',
       'Mo-Fr 07:45-12:00,13:30-18:00;Sa 08:10-13:45',
       'Mo-Fr 09:00-18:00',
       'Mo,Fr 07:00-18:00;Tu-Th 08:00-12:30,13:15-18:00;Sa 08:00-13:15;Su 10:00-15:15',
       'Mo 06:00-15:00,15:30-20:00;Tu-Fr 07:00-12:30,13:30-17:00;Sa 07:00-12:30,13:00-15:00;Su 09:30-15:00',
       'Mo-Fr 07:00-18:30;Sa 09:00-14:00',
       'Mo-Fr 08:00-19:00;Sa 09:00-18:00;Su 10:00-16:00',
       'Mo-Fr 07:45-12:30,13:30-17:45;Sa 09:00-14:00',
       'Mo,Fr 06:00-18:00;Tu-Th 06:45-12:00,12:45-17:00;Su 09:00-12:30,13:00-17:00',
       'Mo-Fr 07:00-19:00;Sa 09:00-17:00;Su 10:00-17:00',
       'Mo-Fr 07:00-12:15,13:00-17:00;Sa 09:00-12:30,13:00-17:00;Su 08:00-12:30,13:00-15:00',
       'Mo-Fr 08:00-12:30,13:30-17:00;Sa 10:00-15:30',
       'Mo-Fr 07:15-12:15,13:00-17:30;Sa-Su 08:10-12:15,12:45-15:50',
       'Mo-Fr 08:00-18:00 open "Wir bedienen Sie auch persönlich im DB Videoreisezentrum"',
       'Mo-Fr 06:00-20:00 open "Veränderte Öffnungszeiten aus betrieblichen Gründen:;Mi, 07.06.23 von 06:00  - 18:00 Uhr;Sa, 10.06.23: 07:00-16:00/So, 11.06.23: 09:00-18:00 Uhr";Sa 07:00-20:00 open "Veränderte Öffnungszeiten aus betrieblichen Gründen:;Mi, 07.06.23 von 06:00  - 18:00 Uhr;Sa, 10.06.23: 07:00-16:00/So, 11.06.23: 09:00-18:00 Uhr";Su 08:00-20:00 open "Veränderte Öffnungszeiten aus betrieblichen Gründen:;Mi, 07.06.23 von 06:00  - 18:00 Uhr;Sa, 10.06.23: 07:00-16:00/So, 11.06.23: 09:00-18:00 Uhr"',
       'Mo-Fr 06:00-21:00;Sa-Su 08:00-20:15', 'Mo-Fr 07:30-18:30',
       'Mo-Fr 07:40-18:30;Sa 08:00-13:30;Su 09:00-14:30',
       'Mo-Fr 06:30-19:00;Sa 08:00-17:30;Su 10:00-18:00',
       'Mo-Fr 07:00-19:00;Sa 09:30-14:30',
       'Mo-Sa 07:00-20:00;Su 08:00-20:00',
       'Mo-Fr 09:00-12:00,13:00-17:15',
       'Mo-Fr 07:45-13:00,13:45-17:30;Sa 07:45-12:45',
       'Mo-Fr 07:00-20:00;Sa-Su 09:00-19:00',
       'Mo-Fr 07:45-12:30,13:30-18:00;Sa 08:45-14:20',
       'Mo-Fr 07:30-18:30;Sa 08:30-15:00;Su 10:00-16:00',
       'Mo 07:00-12:30,13:00-17:00;Tu,Fr 08:00-12:30,13:00-17:00;We-Th 08:00-12:30,13:00-18:00;Sa 07:30-12:30,13:00-15:00;Su 10:00-15:00',
       'Mo,Th 06:30-18:00;Tu-We,Fr 06:30-12:00,13:00-17:00;Sa 06:30-12:00,13:00-15:00',
       'Mo-Fr 06:50-18:00;Sa 08:30-13:00',
       'Mo,Fr 06:45-18:00;Tu-Th 07:20-12:00,13:00-17:30',
       'Mo-Th 08:00-12:00,12:45-16:30;Fr 08:00-13:30',
       'Mo-We,Fr 07:30-11:30,12:30-17:30;Th 06:00-15:00,15:30-20:00;Sa 08:30-14:00;Su 10:00-15:00',
       'Mo-Fr 09:00-13:00,14:00-17:30;Sa 09:00-13:00',
       'Mo-Fr 07:45-12:00,12:45-17:30;Sa 08:00-13:00',
       'Mo 06:00-20:00;Tu-Fr 07:00-20:00;Sa-Su 08:00-13:00,14:00-16:30',
       'Mo-We,Fr 07:30-12:30,13:30-17:30;Th 06:30-15:00,15:30-20:00;Sa-Su 09:30-15:00',
       'Mo-Fr 08:30-12:45,13:45-18:00;Sa 08:30-13:00',
       'Mo-Fr 08:00-12:00,13:00-16:30',
       'Mo 06:00-15:00,15:30-20:00;Tu-Fr 07:30-12:30,13:30-17:30;Sa-Su 09:45-15:00',
       'Mo-Fr 06:30-18:00;Sa 09:00-15:30;Su 11:00-17:00',
       'Mo-Fr 08:00-13:00,13:30-17:00;Sa 08:00-13:00',
       'Mo,Th-Fr 08:00-16:30;Tu-We 08:00-12:30,13:00-17:00;Sa 08:00-12:30,13:00-16:00',
       'Mo,Th-Fr 07:15-11:45,12:00-17:00;Tu-We 07:15-11:45,12:30-17:00;Su 10:00-15:00',
       'Mo 12:30-18:00,8:00-12:00;Tu-Fr 08:00-12:00,12:30-18:00;Sa 08:00-12:00',
       'Mo-Fr 07:00-19:00;Sa-Su 08:00-18:00',
       'Mo-Fr 07:00-12:00,14:00-18:00;Sa 09:00-14:00',
       'Mo-Fr 06:30-21:00;Sa 08:00-19:00;Su 09:00-20:00',
       'Mo-Fr 08:00-12:30,13:30-18:00;Sa 09:00-13:00',
       'Mo,We-Fr 07:00-12:45,13:45-17:00;Tu 06:00-15:00,15:45-20:00;Sa-Su 09:30-15:00',
       'Mo-Fr 08:00-12:00,12:45-17:00',
       'Mo-Fr 07:00-19:30;Sa-Su 09:15-17:45',
       'Mo-Fr 08:00-17:00;Sa 08:00-13:00',
       'Mo-Fr 08:00-12:00,13:00-16:00',
       'Mo-Fr 08:00-18:30;Sa 08:30-16:30',
       'Mo-Fr 07:00-18:00;Sa 08:00-13:00',
       'Mo-Fr 05:45-20:30;Sa-Su 06:45-20:00',
       'Mo-Fr 08:00-13:00,14:00-18:00;Sa 08:30-13:00',
       'Mo 06:00-19:30;Tu-Fr 07:00-19:30;Sa 08:00-17:00;Su 09:00-17:30',
       'Mo-Fr 06:00-21:00;Sa-Su 07:00-21:00',
       'Mo 06:00-19:00 open "08.06.2023 Fronleichnam 10:00 - 17:00 ";Tu-Th 07:00-19:00 open "08.06.2023 Fronleichnam 10:00 - 17:00 ";Fr 07:00-20:00 open "08.06.2023 Fronleichnam 10:00 - 17:00 ";Sa 09:00-17:00 open "08.06.2023 Fronleichnam 10:00 - 17:00 ";Su 10:00-18:00 open "08.06.2023 Fronleichnam 10:00 - 17:00 "',
       'Mo 07:30-12:00,12:45-16:30;Tu-Fr 08:30-12:00,12:45-16:30',
       'Mo-Fr 07:45-12:15,13:00-17:45',
       'Mo-Fr 09:00-12:00,13:00-17:25 open "Bitte Beachten: ;Am Montag, 31.07.23 geschlossen "',
       'Mo-Fr 06:50-12:30,13:15-17:05;Sa 07:30-11:00,11:30-15:30;Su 08:45-12:00,12:30-17:15',
       'Mo-Fr 08:00-18:00;Sa 09:00-13:00',
       'Mo-Fr 08:00-12:00,13:00-17:00',
       'Mo-Fr 06:00-18:00;Sa 07:00-14:00;Su 08:30-16:00',
       'Mo-Fr 08:00-12:00,12:45-17:45;Sa 08:00-13:00',
       'Mo-Fr 08:45-12:00,12:45-17:00',
       'Mo-Fr 08:00-20:00;Sa 10:00-20:00;Su 10:00-18:00',
       'Mo-Fr 08:00-18:00 open "Wir bedienen Sie auch gerne in unserem Videoreisezentrum"',
       'Mo 07:00-11:30,12:15-16:00;Tu-Fr 08:00-12:15,13:00-16:00',
       'Mo-Fr 08:30-12:15,12:45-16:00',
       'Mo-Fr 07:00-19:00;Sa 08:00-16:00;Su 10:00-19:00',
       'Mo-Fr 07:00-12:30,13:30-18:00;Sa 09:00-14:00',
       'Mo-Fr 07:30-18:15 open "Bitte Beachten:;Am Montag, den 30.07.2023;07:30 - 11:45 und 12:30 - 16:00 Uhr";Sa 08:00-15:00 open "Bitte Beachten:;Am Montag, den 30.07.2023;07:30 - 11:45 und 12:30 - 16:00 Uhr";Su 10:00-15:00 open "Bitte Beachten:;Am Montag, den 30.07.2023;07:30 - 11:45 und 12:30 - 16:00 Uhr"',
       'Mo-Fr 08:30-13:00,14:00-18:00;Sa 08:30-13:00',
       'Mo-Fr 05:45-20:00;Sa 06:30-19:30;Su 08:30-19:30',
       'Mo-Fr 07:30-18:30 open "Wir bedienen Sie auch gerne in unserem Videoreisezentrum";Sa 09:00-14:00 open "Wir bedienen Sie auch gerne in unserem Videoreisezentrum"',
       'Mo-Fr 08:00-11:30,12:00-17:00;Sa-Su 08:00-11:30,12:00-15:30',
       'Mo-Fr 07:30-18:30;Sa 07:30-11:30,12:00-15:30;Su 09:30-13:30,14:00-17:30',
       'Mo-Fr 06:00-20:00;Sa 07:00-19:00;Su 08:00-20:00',
       'Mo-Fr 06:00-21:15;Sa 07:00-19:00;Su 08:00-20:00',
       'Mo-Fr 06:00-21:00;Sa-Su 08:00-20:30',
       'Mo-Fr 08:00-19:00;Sa 08:00-16:00;Su 09:00-16:00',
       'Mo-Fr 08:00-12:15,12:45-16:00',
       'Mo-Fr 06:45-18:35;Sa 07:30-13:00',
       'Mo 06:00-10:30,11:30-16:00;Tu,Th-Fr 08:30-12:30,13:30-16:00;We 09:30-12:30,13:30-19:00;Sa 08:00-12:00',
       'Mo-Fr 08:00-12:00,12:30-18:00;Sa 08:00-12:00',
       '24/7 closed "Dauerhaft geschlossen"', 'Mo-Fr 06:15-16:40',
       'Mo-Fr 07:00-20:00;Sa-Su 09:00-18:00',
       'Mo-Fr 06:50-20:00;Sa-Su 07:50-19:00',
       'Mo-Fr 07:00-20:00;Sa 08:00-18:00;Su 08:00-13:00,14:00-18:00',
       'Mo-Fr 07:00-20:00;Sa-Su 08:00-18:00',
       'Mo-Fr 08:00-12:00,13:00-16:30 open "Nutzen Sie auch das Video-Reisezentrum im Vorraum"',
       'Mo-Fr 06:00-10:30,11:00-15:30,16:00-21:00;Sa-Su 06:45-15:30,16:00-19:45',
       'Mo-We 08:00-12:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten.";Th-Fr 13:00-17:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten."',
       'Mo-Fr 07:00-17:00;Sa 08:00-13:00',
       'Mo-Fr 09:00-13:15,13:45-17:00',
       'Mo-Fr 07:00-18:30;Sa 08:00-12:00,12:30-16:00;Su 10:00-15:30',
       'Mo,We-Fr 08:00-12:30,13:30-18:00;Tu 06:30-15:00,15:30-20:00;Sa-Su 09:30-15:00',
       'Mo-Fr 08:30-12:20,12:50-16:30',
       'Mo-We,Fr 07:00-12:45,13:45-17:00;Th 06:00-15:00,15:45-20:00;Sa-Su 09:30-15:00',
       'Mo-Fr 06:15-12:00,12:45-16:00;Sa 07:45-12:45',
       'Mo-Fr 08:30-13:00,14:00-18:30;Sa 08:30-13:30',
       'Mo-Fr 08:30-12:30,14:00-17:00',
       'Mo,Fr 06:30-19:15 open "gültig ab 01.01.22";Tu-Th 07:30-12:30,13:15-17:15 open "gültig ab 01.01.22";Su 10:45-14:00,14:30-19:00 open "gültig ab 01.01.22"',
       'Mo-Fr 07:00-21:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten.";Sa 09:00-19:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten.";Su 10:00-20:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten."',
       'Mo-Fr 09:00-12:30,13:00-16:45', 'Mo-Fr 09:10-12:15,13:30-17:50',
       'Mo-Fr 07:00-20:00;Sa 08:00-18:00;Su 09:00-20:00',
       'Mo-Fr 09:00-12:00,13:00-17:00 open "Nutzen Sie auch das Video-Reisezentrum im Vorraum"',
       'Mo-Fr 09:00-12:30,13:00-18:00;Sa 09:00-13:00,13:30-16:00;Su 12:00-15:30,16:00-20:00',
       'Mo-Fr 08:00-18:00;Sa 09:00-14:00',
       'Mo-Fr 07:30-19:00;Sa 09:00-18:00;Su 10:00-18:30',
       'Mo-Fr 06:30-18:00;Sa 08:00-17:00;Su 10:00-13:30',
       'Mo-Fr 07:00-18:00;Sa-Su 10:00-15:15',
       'Mo-Fr 08:30-12:00,12:45-16:15 open "Nutzen Sie auch das Video-Reisezentrum am Vorplatz"',
       'Mo-Fr 07:00-19:00;Sa 08:00-17:00;Su 09:00-18:00',
       'Mo-Fr 09:00-13:00,13:30-17:00;Sa 08:00-13:00',
       'Mo-Fr 06:15-12:00,12:35-16:50;Sa 08:30-14:00;Su 09:30-13:30',
       'Mo-Th 08:00-18:00;Fr 08:00-13:00,14:00-18:00',
       'Mo-Fr 10:00-16:00',
       'Mo-Fr 08:00-12:30,13:15-18:00;Sa-Su 08:45-11:45,12:15-16:15',
       'Mo-Fr 06:45-18:45;Sa 07:45-15:30',
       'Mo-Fr 07:30-18:00;Sa 09:00-14:00;Su 09:30-13:30,14:00-17:00',
       'Mo-Fr 08:15-12:15,13:00-17:45;Sa 07:15-12:30,13:00-15:45',
       'Mo-Fr 07:30-18:30;Sa 08:30-14:00;Su 11:30-16:30',
       'Mo 07:45-12:45,13:45-17:00;Tu,Th-Fr 07:00-12:45,13:45-17:00;We 06:00-15:00,15:45-20:00;Sa-Su 09:30-15:00',
       'Mo-Fr 07:30-13:00,14:00-18:00;Sa-Su 08:15-13:00,13:30-16:00',
       'Mo-Fr 06:30-19:00;Sa 08:30-14:05',
       'Mo-Fr 08:30-13:00,14:00-18:30;Sa 08:30-12:30',
       'Mo-Fr 06:30-18:30;Sa 07:30-12:30;Su 11:30-16:30',
       'Mo-Fr 08:00-20:00;Sa-Su 09:00-19:00',
       'Mo-Fr 08:00-20:00;Sa-Su 10:00-20:00',
       'Mo-Fr 07:00-18:00;Sa 08:30-16:00;Su 09:30-17:00',
       'Mo-Fr 06:00-20:00;Sa 08:00-17:00;Su 09:00-18:00',
       'Mo-Fr 08:30-13:15,14:15-18:10;Sa 08:30-13:15',
       'Mo-Th 07:15-12:00,12:30-15:45',
       'Mo-Fr 06:00-21:15;Sa-Su 07:00-19:00',
       'Mo-We,Fr 07:30-13:00,14:00-17:00;Th 06:00-15:00,15:30-20:00;Sa 07:30-11:00,11:30-15:00;Su 09:30-15:00',
       'Mo-Fr 07:45-12:45,13:30-18:00;Sa-Su 08:30-12:30,13:00-15:15',
       'Mo-Fr 08:00-18:00 open "Bitte beachten Sie unsere geänderten Öffnungszeit am ;21.07.23 von 08:00-12:30 und 13:00-16:00 Uhr; Wir bedienen Sie auch persönlich im DB Videoreisezentrum "',
       'Mo-Fr 06:30-18:00 open "Wir danken für Ihr Verständnis";Sa 08:30-13:30 open "Wir danken für Ihr Verständnis"',
       'Mo-Fr 08:30-11:30,12:30-16:55 open " Wir bedienen Sie auch persönlich im DB Videoreisezentrum ;am Bahnsteig 1. "',
       'Mo-Fr 06:30-12:10,12:55-16:50;Sa 06:45-12:15',
       'Mo-Fr 08:00-18:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten.";Sa 08:00-13:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten."',
       'Mo-Fr 06:00-19:00;Sa 08:00-17:00;Su 09:00-18:00',
       'Mo-Fr 07:00-19:00;Sa 08:30-17:45;Su 09:30-18:00',
       'Mo-Fr 08:50-12:30,13:45-17:30',
       'Mo-Tu,Th-Fr 07:30-11:50,12:40-15:40',
       'Mo-Fr 07:00-19:00;Sa 08:00-16:00;Su 09:30-18:30',
       'Mo-Fr 08:30-12:30,13:45-17:10',
       'Mo-Fr 07:00-19:00;Sa 08:00-17:30;Su 10:00-15:30',
       'Mo-Tu,Th-Fr 08:00-12:30,13:30-18:00;We 06:30-15:00,15:30-20:00;Sa-Su 09:30-15:00',
       'Mo-Fr 08:15-17:30;Sa 09:15-13:00',
       'Mo-Fr 08:00-12:30,13:30-17:30;Sa 08:30-12:30',
       'Mo 06:00-17:30;Tu-Fr 07:30-17:30;Sa 07:00-09:45,10:15-13:30;Su 10:00-17:30',
       'Mo-Fr 06:15-20:15;Sa-Su 08:15-18:15',
       'Mo-Fr 06:00-22:00;Sa-Su 07:00-22:00',
       'Mo-Fr 08:00-18:00;Sa 09:00-18:00',
       'Mo-Fr 07:00-18:30;Sa 08:30-14:00',
       'Mo 08:15-12:30,13:00-16:15;Tu-Fr 08:00-12:30,13:00-15:30;Sa 08:15-13:15'],
      dtype=object)
In [ ]:
def getDays(daysList):
    days_of_week = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su']
    start_index = days_of_week.index(daysList[0])
    end_index = days_of_week.index(daysList[-1])
    
    if start_index <= end_index:
        days_between = days_of_week[start_index:end_index+1]
    else:
        days_between = days_of_week[start_index:] + days_of_week[:end_index+1]
    
    return days_between
In [ ]:
def get_minute_intervals(interval, time_range):
    start_str, end_str = time_range.split('-')
    start_time = datetime.strptime(start_str, '%H:%M')
    end_time = datetime.strptime(end_str, '%H:%M')
    
    interval = timedelta(minutes=interval)
    current_time = start_time
    intervals = []
    
    while current_time <= end_time:
        intervals.append(current_time.strftime('%H:%M'))
        current_time += interval
    
    return intervals

Mein Ziel war es, einen DataFrame mit den Öffnungszeiten in Stunden pro Tag pro ID zu erstellen.

Aktuell beinhalten die Daten nur die Öffnungszeiten pro Tag, ohne die genauen Uhrzeiten. Ich müsste vermutlich eine doppelte x-Achse verwenden, um sowohl die Tag- als auch die Uhrzeiten anzuzeigen.

In [ ]:
data_rows = []
for entry in df_travel_center['openingHours']:
    day_time_pairs = entry.split(';')
    row = {}
    for pair in day_time_pairs:
        try:
            days, times = pair.split(' ')
            days = days.split('-')

            fixed_list = []
            for item in days:
                if (',' in item):
                    day_item = item.split(',')
                    fixed_list.append(day_item)
                else:
                    fixed_list.append(item)
            
            allDays = getDays(fixed_list)
            #print(allDays)
            for day in allDays:
                row[day] = 1
        except Exception as e:
            #print(e)
            #times = pair.split(' ')
            #print(times)
            row[day] = 0
    data_rows.append(row)

df_opening_hours = pd.DataFrame(data_rows)
df_opening_hours
Out[ ]:
Mo Tu We Th Fr Sa Su
0 1.0 1.0 1.0 1.0 1.0 1.0 1.0
1 1.0 1.0 1.0 1.0 1.0 1.0 NaN
2 1.0 1.0 1.0 1.0 1.0 1.0 1.0
3 0.0 NaN NaN 1.0 NaN 1.0 NaN
4 1.0 1.0 1.0 1.0 1.0 1.0 NaN
... ... ... ... ... ... ... ...
255 1.0 1.0 1.0 1.0 1.0 1.0 1.0
256 1.0 1.0 1.0 1.0 1.0 1.0 NaN
257 1.0 1.0 1.0 1.0 1.0 1.0 NaN
258 1.0 1.0 1.0 1.0 1.0 NaN NaN
259 1.0 1.0 1.0 1.0 1.0 1.0 NaN

260 rows × 7 columns

Man kann sehen, dass in der Tabelle des öfteren NaN auftaucht. Wenn die Daten nicht verarbeitet werden konnten, lag es daran, dass dort noch Kommentare in den Zeiten vorhanden waren, wie beispielsweise aufgrund von Krankheit geschlossen.
Daher gehen wir davon aus, dass NaN ebenfalls geschlossen bedeutet.

In [ ]:
df_opening_hours.replace(np.nan, 0, inplace=True)
df_opening_hours
Out[ ]:
Mo Tu We Th Fr Sa Su
0 1.0 1.0 1.0 1.0 1.0 1.0 1.0
1 1.0 1.0 1.0 1.0 1.0 1.0 0.0
2 1.0 1.0 1.0 1.0 1.0 1.0 1.0
3 0.0 0.0 0.0 1.0 0.0 1.0 0.0
4 1.0 1.0 1.0 1.0 1.0 1.0 0.0
... ... ... ... ... ... ... ...
255 1.0 1.0 1.0 1.0 1.0 1.0 1.0
256 1.0 1.0 1.0 1.0 1.0 1.0 0.0
257 1.0 1.0 1.0 1.0 1.0 1.0 0.0
258 1.0 1.0 1.0 1.0 1.0 0.0 0.0
259 1.0 1.0 1.0 1.0 1.0 1.0 0.0

260 rows × 7 columns

Generell sieht man, dass die meisten Reisezentren am Wochenende geschlossen haben, vor allem am Sonntag. Vereinzelt sind aber auch unter der Woche Reisezentren geschlossen. Donnerstags scheint der Tag zu sein, an dem die meisten geöffnet haben.

In [ ]:
custom_color_scale = [
    [0.0, 'rgb(255, 0, 0)'],     # closed = red
    [1.0, 'rgb(0, 255, 0)']      # open = green
]

fig = go.Figure(data=go.Heatmap(
    z=[[col for col in row] for _, row in df_opening_hours.iterrows()],
    x=df_opening_hours.columns,
    y=df_opening_hours.index,
    colorscale=custom_color_scale
))

fig.update_layout(
    title='Öffnungszeiten Reisezentren'
)

fig.show()

Hier sieht man dies noch einmal deutlicher, allerdings sind die Werktage unter der Woche alle ziemlich gleich, sodass es sich auch um Ungenauigkeiten in den Daten handeln kann.

In [ ]:
df_opening_hours_grouped = df_opening_hours.sum().sort_values()
px.bar(df_opening_hours_grouped)

Nun können wir noch die Informationen der bahnhofsnahmen Dienstleistungen unserer Karte hinzufügen.
Dazu müssen die Dataframes zunächst wieder zusammengebracht werden.

In [ ]:
df_station_facilities_by_id = df_station_facilities.groupby('id')['type'].unique().reset_index()
df_station_facilities_by_id.head(1)
Out[ ]:
id type
0 1 [MOBILE_TRAVEL_SERVICE, TRIPLE_S_CENTER, RAILW...
In [ ]:
df_station_facilities_by_id['id'] = df_station_facilities_by_id['id'].astype(int)
In [ ]:
# del map_df_extended
In [ ]:
map_df_extended = pd.merge(left=map_df, right=df_station_facilities_by_id, on=['id'], how='left')
In [ ]:
map_df_extended.head(1)
Out[ ]:
id city countryCode houseNumber latitude longitude metropolis name organisationalUnit owner postalCode state stationCategory street availableTransports transportAssociations image type
0 1 Aachen DE 2a 50.7678 6.091499 {} Aachen Hbf RB West DB S&S 52064 Nordrhein-Westfalen CATEGORY_2 Bahnhofstr. [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... [AAV, VRS] https://api.railway-stations.org/photos/de/1_1... [MOBILE_TRAVEL_SERVICE, TRIPLE_S_CENTER, RAILW...

Da in einigen Reihen von type NaN-Werte vorhanden sind, müssen wir dies zunächst austauschen. Immer wenn ein Wert nicht vom Typ Numpy Array ist (also NaN), erstellen wir eine neue leere Liste, um keine Daten zu verlieren.

In [ ]:
def replace_nan_with_empty_array(value):
    if isinstance(value, np.ndarray):
        return value
    else:
        return np.array([])
In [ ]:
map_df_extended['type'] = map_df_extended['type'].apply(replace_nan_with_empty_array)
map_df_extended

Jetzt wird wieder für jeden Typ eine FeatureGroup für die Karte erstellt.

In [ ]:
facilities_dict = {}
for i in map_df_extended_clean.index:
    facilities_dict.setdefault(map_df_extended_clean['type'][i][0], folium.FeatureGroup(name=map_df_extended_clean['type'][i][0], show=False, autoZIndex=False)) # TODO: current cheat: we always take only first value
facilities_dict
Out[ ]:
{'MOBILE_TRAVEL_SERVICE': <folium.map.FeatureGroup at 0x1b6e13ec970>,
 'CAR_PARKING': <folium.map.FeatureGroup at 0x1b68bda7eb0>,
 'TRIPLE_S_CENTER': <folium.map.FeatureGroup at 0x1b68bda7fd0>,
 'LOCKER': <folium.map.FeatureGroup at 0x1b6a8240a00>,
 'BICYCLE_PARKING': <folium.map.FeatureGroup at 0x1b6a8240b80>,
 'RAD_PLUS': <folium.map.FeatureGroup at 0x1b6a82471f0>,
 'HANDICAPPED_TRAVELLER_SERVICE': <folium.map.FeatureGroup at 0x1b6e12cb4c0>,
 'PUBLIC_RESTROOM': <folium.map.FeatureGroup at 0x1b6a14ede20>,
 'TRAVEL_NECESSITIES': <folium.map.FeatureGroup at 0x1b6a04acf70>,
 'RAILWAY_MISSION': <folium.map.FeatureGroup at 0x1b6a04ac430>,
 'INFORMATION_COUNTER': <folium.map.FeatureGroup at 0x1b680405be0>,
 'TRAVEL_CENTER': <folium.map.FeatureGroup at 0x1b6d886a430>,
 'TAXI_RANK': <folium.map.FeatureGroup at 0x1b6a06a3040>,
 'VIDEO_TRAVEL_CENTER': <folium.map.FeatureGroup at 0x1b6a06a3070>,
 'CAR_RENTAL': <folium.map.FeatureGroup at 0x1b6a06a30a0>,
 'WIFI': <folium.map.FeatureGroup at 0x1b6a06a30d0>}

Letztlich werden beide Feature Groups, die von den Bundesländern und die der Einrichtungen zusammen auf die Karte gebracht.

In [ ]:
map_df = map_df_extended_clean

m = folium.Map(location=[50.111, 8.682],zoom_start=6) # limit with width=1500,height=1500 produces just white space around the map.

for i in map_df.index:
    html=f"""
    <img src="{map_df['image'][i]}" width="500px">
    <br/>
    <b><p>{map_df['id'][i]}: {map_df['name'][i]}</b></p>
    <p>Transports: {map_df['availableTransports'][i]}</p>
    <p>Associations: {map_df['transportAssociations'][i]}</p>
    <p>Local services: {map_df['type'][i]}</p>
    """

    parsedHtml = folium.Html(html, script=True)
    popup = folium.Popup(parsedHtml, max_width=2650)

    feature_group = state_dict[map_df['state'][i]]
    m.add_child(feature_group)

    feature_group = facilities_dict[map_df['type'][i][0]] # TODO: current cheat: we always take only first value
    m.add_child(feature_group)

    folium.Marker(
        location=[ map_df['latitude'][i], map_df['longitude'][i] ], 
        icon=GetIcon(map_df['availableTransports'][i]),
        radius=8,
        tooltip=map_df['name'][i],
        popup=popup
    ).add_to(feature_group)
    
folium.LayerControl(collapsed=False).add_to(m)
m
Out[ ]:
Make this Notebook Trusted to load map: File -> Trust Notebook

Parkplätze¶

Ein weiterer sehr interessanter Datensatz sind Parkplätze, die ebenfalls von der DB Bahnpark betrieben werden. Zum Teil wird die Verfügbarkeit der Parkplätze in Echtzeit angegeben sowie die Preise der Parkplätze ausgewiesen.

In [ ]:
pkl_file = open(data_folder + 'parking_facilities.pkl', 'rb')
df_parking = pickle.load(pkl_file)
pkl_file.close()
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
d:\zail\python4ds\python4ds_project\DB_EDA.ipynb Cell 116 in 1
----> <a href='vscode-notebook-cell:/d%3A/zail/python4ds/python4ds_project/DB_EDA.ipynb#Y223sZmlsZQ%3D%3D?line=0'>1</a> pkl_file = open('parking_facilities.pkl', 'rb')
      <a href='vscode-notebook-cell:/d%3A/zail/python4ds/python4ds_project/DB_EDA.ipynb#Y223sZmlsZQ%3D%3D?line=1'>2</a> df_parking = pickle.load(pkl_file)
      <a href='vscode-notebook-cell:/d%3A/zail/python4ds/python4ds_project/DB_EDA.ipynb#Y223sZmlsZQ%3D%3D?line=2'>3</a> pkl_file.close()

File c:\Users\lukas\anaconda3\envs\stats\lib\site-packages\IPython\core\interactiveshell.py:282, in _modified_open(file, *args, **kwargs)
    275 if file in {0, 1, 2}:
    276     raise ValueError(
    277         f"IPython won't let you open fd={file} by default "
    278         "as it is likely to crash IPython. If you know what you are doing, "
    279         "you can use builtins' open."
    280     )
--> 282 return io_open(file, *args, **kwargs)

FileNotFoundError: [Errno 2] No such file or directory: 'parking_facilities.pkl'
In [ ]:
 

Fazit¶

Es ist schon unglaublich, was die Deutsche Bahn alles leistet. Dazu gehören nicht nur fahrende Züge, sondern die komplette Mobilitätsinfrastruktur.